 <template>
    <div class="iperf-container">
         <el-form
         :model="iperfFormData"
         :rules="iperfFormRules"
         ref="iperfFormRef"
         class="tech-form"
         hide-required-asterisk
         >
          <el-form-item 
          v-for="(item,key) in iperfFormItems" 
          :key="key" 
          :label="item.label" 
          :prop="item.prop"
          v-show="item.isShow"
          class="tech-form-item"
          >
            <component 
            :is="item.component" 
            v-model="iperfFormData[key]" 
            :placeholder="item.placeholder" 
            v-bind="item.bindProps || {}"
            class="tech-input"
            />
          </el-form-item>
         </el-form>
        <div class="btn-group">
            <div v-for="(item,index) in iperfButtonGroup" 
            :key="index" 
            @click="handleIperfButtonClick(item.key)"
            :class="['tech-btn', {'disabled': item.disabled}]"
            v-show="item.key !== 'stop' || stopButtonVisible"
            >
            <el-icon><component :is="item.icon" /></el-icon>
              <span>{{ item.label }}</span>
            </div>
        </div>
        <div class="log_container tech-log" ref="iperfLogContainerRef" @scroll="iperfScroll">
          <div v-for="(item,index) in iperfResult" :key="item">
            <div class="log-item">
              <span class="log-index">[{{index+1}}]</span>
              <span class="log-content">{{item}}</span>
            </div>
          </div>
        </div>
        <transition name="fade">
            <div class="scroll-btn" v-if="iperfResult.length > 0&&!isAtBottom">
              <div class="tech-btn tech-scroll-btn" @click="scrollToBottom">
                <span>滚动到底部</span>
                <el-icon><ArrowDown /></el-icon>
              </div>
            </div>
      </transition>
      </div>
 </template>
 
 <script lang="ts">
 import { defineComponent } from 'vue'
 export default defineComponent({
   name: 'IperfComponents',
 })
 </script>
 <script setup  lang="ts">
 import { useI18n } from 'vue-i18n'
import MSelect from '@/components/MSelect.vue'
import {getWebSocketInstance} from '@/utils/websocketExample'
import { ElNotificationFn } from '@/utils/computedUtils'
import { CaretRight, CircleClose, Delete, ArrowDown } from '@element-plus/icons-vue'
import { throttle } from 'lodash'
const props = defineProps<{
  direction:string
}>()
const {direction} = toRefs(props)
const isAtBottom = ref(true) // 内容是否滚动到底部,默认true,为true表示到了底部没有手动滚动不显示按钮,为false表示手动滚动显示滚动按钮
const iperfLogContainerRef = ref<any>(null) 
const iperfIsManualScroll = ref(false)//区分是否用户手动滚动
const iperfStartStatus = ref<any>({ // 区分服务端与客户端是否开始测试和测试完成
  server:false,
  client:false
})
const stopButtonVisible = computed(()=>{ //控制停止按钮是否显示
  return iperfStartStatus.value[direction.value]
})
async function scrollToBottom () {
  if (iperfLogContainerRef.value) { 
    const container = iperfLogContainerRef.value
    await nextTick()
    const scrollHeight = container.scrollHeight
    const clientHeight = container.clientHeight
    if(isAtBottom.value){
      container.scrollTop = scrollHeight - clientHeight
    }else{
      container.scrollTo({
        top: scrollHeight - clientHeight,
        behavior: 'smooth'
      })
    }
  }
  isAtBottom.value = true
  iperfIsManualScroll.value = false
}
const wsClient = getWebSocketInstance()
const createFormRule = (messageKey: string) => [{
  required: true,
  message: t(messageKey)
}]
const validateForm = async (formRef: any) => {
  if (!formRef.value) return false
  try {
    await formRef.value.validate()
    return true
  } catch (error) {
    return false
  }
}
const { t } = useI18n()
const iperfFormData = ref<any>({
  mode:"",
  time: '',
  bandwidth:'' ,
  destaddr: "",
  port: "",
  length:'' 
})
const iperfFormRef = ref<any>(null)
const modeOptions = computed(()=>{
  return {
    options:[
      {
        label:'TCP',
        value:'tcp'
      },
      {
        label:'UDP',
        value:'udp'
      }
    ],
    width:'100%'
  }
})
const fieldVisibility = ref<any>({
  bandwidth: true,
  time: true,
  destaddr: true,
  length: true
})
const iperfFormItems = computed(()=>{
  return {
    mode:{
      label:t('networkConfig.protocol_type_label'),
      prop:'mode',
      component:MSelect,
      placeholder:t('networkConfig.protocol_type_label'),
      bindProps:modeOptions.value,
      isShow:true
    },
    time:{
      label:t('networkConfig.test_duration_label'),
      prop:'time',
      component:'el-input',
      placeholder:t('networkConfig.test_duration_label'),
      isShow:fieldVisibility.value.time
    },
    bandwidth:{
      label:t('networkConfig.test_rate_label'),
      prop:'bandwidth',
      component:'el-input',
      placeholder:t('networkConfig.test_rate_label'),
      isShow:fieldVisibility.value.bandwidth
    },
    destaddr:{
      label:t('networkConfig.destination_address_label'),
      prop:'destaddr',
      component:'el-input',
      placeholder:t('networkConfig.destination_address_label'),
      isShow:fieldVisibility.value.destaddr
    },
    port:{
      label:t('networkConfig.specified_port_label'),
      prop:'port',
      component:'el-input',
      placeholder:t('networkConfig.specified_port_label'),
      isShow:true
    },
    length:{
      label:t('networkConfig.test_packet_length_label'),
      prop:'length',
      component:'el-input',
      placeholder:t('networkConfig.test_packet_length_label'),
      isShow:fieldVisibility.value.length
    }
  }
})
const createPortRule = (messageKey: string) => [
  {
    required: true,
    message: t(messageKey)
  },
  {
    validator: (_: any, value: any, callback: any) => {
      const port = Number(value)
      if (isNaN(port) || port < 0 || port > 65535) {
        callback(new Error(t('networkConfig.port_range_invalid')))
      } else {
        callback()
      }
    },
    trigger: 'blur'
  }
]
const iperfFormRules = computed(() => {
  const baseRules = {
    direction: createFormRule('networkConfig.test_direction_label'),
    mode: createFormRule('networkConfig.protocol_type_label'),
    port: createPortRule('networkConfig.specified_port_label')
  }
  return baseRules
})
const iperfButtonGroup = computed(()=>{
  return [
    {
      label: iperfStartStatus.value[direction.value] ? "开始测试中..." : t('networkConfig.start_test_label'),
      key: 'start',
      icon: CaretRight,
      disabled: iperfStartStatus.value[direction.value]
    },
    {
      label: t('networkConfig.stop_test_label'),
      key: 'stop',
      icon: CircleClose
    },
    {
      label: t('networkConfig.clear_log_label'),
      key: 'clear',
      icon: Delete
    }
  ]
})
const IPERF_BUTTON_KEY:any = {
  start:handleStartIperf,
  stop:handleStopIperf,
  clear:handleClearIperfLog
}

function handleIperfButtonClick(key:string){
  IPERF_BUTTON_KEY[key]()
}
async function handleStartIperf(){
  const res = await validateForm(iperfFormRef)
  if(!res){
    ElNotificationFn('warning',t('networkConfig.info_not_complete_label'))
    return
  }
  iperfStartStatus.value[direction.value] = true
  const requestData = {...iperfFormData.value,direction:direction.value}
  requestData.time = Number(requestData.time)
  requestData.bandwidth = Number(requestData.bandwidth)
  requestData.length = Number(requestData.length)
  requestData.port = Number(requestData.port)
  if(requestData.direction === 'server'){
    delete requestData.time
    delete requestData.bandwidth
    delete requestData.destaddr
    delete requestData.length
  }
  if(requestData.direction === 'client'&&requestData.mode === 'tcp'){
    delete requestData.bandwidth
    delete requestData.length
  }
  const command = 'iperf-start'
  lastStartDirection.value = direction.value
  wsClient.send({command,data:{...requestData}})
}
const lastStopDirection = ref('')  //添加一个标志来记录最后一次停止的方向
const lastStartDirection = ref('')  //添加一个标志来记录最后一次启动的方向
function handleStopIperf(){
  lastStopDirection.value = direction.value
  wsClient.send({command:'iperf-stop',data:{direction:direction.value}})
}
function handleClearIperfLog(){
  resultMap[direction.value].length = 0 // 清空数组但保留引用
}
const scrollTop = ref<number>(0)
const throttledScrollHandler = throttle((event: any) => {
  const target = event.target;
  scrollTop.value = target.scrollTop
  const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 20;
  // isAtBottom为true,表示到了底部没有手动滚动,isAtBottom为false,则iperfIsManualScroll为true,代表手动滚动
  iperfIsManualScroll.value = !isNearBottom;
  isAtBottom.value = isNearBottom
}, 200)
function iperfScroll(event: any) {
    throttledScrollHandler(event)
}
watch(()=>direction.value,(newVal:any)=>{
  if(newVal === 'server'){
    for(let key in fieldVisibility.value){
      fieldVisibility.value[key] = false
    }
  } else {
    if(iperfFormData.value.mode === 'tcp'){
      for(let key in fieldVisibility.value){
        fieldVisibility.value[key] = false
      }
      fieldVisibility.value.destaddr = true
      fieldVisibility.value.time = true
    } else {
      for(let key in fieldVisibility.value){
        fieldVisibility.value[key] = true
      }
    }
  }
},{
    immediate:true
})
watch(()=>iperfFormData.value.mode,(newVal:any)=>{
  if(direction.value === 'server') return
  if(newVal === 'tcp'){
    for(let key in fieldVisibility.value){
      fieldVisibility.value[key] = false
    }
    fieldVisibility.value.destaddr = true
    fieldVisibility.value.time = true
  }else{
    for(let key in fieldVisibility.value){
      fieldVisibility.value[key] = true
    }
  }
})
const resultMap = reactive<any>({
  'server': [],
  'client': []
})
const iperfResult = computed(():any[] => {
  return resultMap[direction.value]
})
function handleIperfStartResponse(data: any) {
  resultMap['server'].push(data.msg)
  if (!iperfIsManualScroll.value || isAtBottom.value) {
      scrollToBottom()
    }
  }
function handleIperfStopResponse() {
   if(lastStopDirection.value){
    const message = lastStopDirection.value === 'server' 
      ? t('networkConfig.stop_iperf_server_success_label') 
      : t('networkConfig.stop_iperf_client_success_label')
    ElNotificationFn('success', message)
    iperfStartStatus.value[lastStopDirection.value] = false
    lastStopDirection.value = '' // 重置标志
    lastStartDirection.value = ''
   }
}
function handleIperfServerDoneResponse() {
  iperfStartStatus.value['server'] = false
  if (!iperfIsManualScroll.value || isAtBottom.value) {
    scrollToBottom()
  }
  ElNotificationFn('success',t('networkConfig.iperf_server_done_label'))
}
function handleIperfClientDoneResponse() {
  iperfStartStatus.value['client'] = false
  if (!iperfIsManualScroll.value || isAtBottom.value) {
    scrollToBottom()
  }
  ElNotificationFn('success',t('networkConfig.iperf_client_done_label'))
}
function handleIperfClientStartResponse(data: any) {
  resultMap['client'].push(data.msg)
  if (!iperfIsManualScroll.value) {
      scrollToBottom()
    }
}
const wsResponseHandlers:any = {
  'server':handleIperfStartResponse,
  'client':handleIperfClientStartResponse,
  'iperf-stop':handleIperfStopResponse,
  'server-done':handleIperfServerDoneResponse,
  'client-done':handleIperfClientDoneResponse
}
function onWebSocketMessage(response: any) {
  const { cmd, data } = response
  if (!Object.keys(wsResponseHandlers).includes(cmd)) return
  wsResponseHandlers[cmd](data)
}
wsClient.on('message',onWebSocketMessage)
onActivated(()=>{
    iperfLogContainerRef.value.scrollTop = scrollTop.value
})
</script>
 
 <style lang="scss" scoped>
@import '@/styles/input.scss';
@import '@/styles/form.scss';
@import '@/styles/log_container.scss';

.iperf-container {
  padding: 15px;
}

.tech-form {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 15px;
  margin-bottom: 20px;
}

.tech-form-item {
  position: relative;
  margin-bottom: 0 !important;
  
  :deep(.el-form-item__label) {
    font-size: 14px;
    color: #00b4ff;
    text-shadow: 0 0 5px rgba(0, 100, 255, 0.3);
    position: relative;
    padding-left: 12px;
    margin-bottom: 5px;
    
    &:before {
      content: '';
      position: absolute;
      left: 0;
      top: 50%;
      transform: translateY(-50%);
      width: 4px;
      height: 14px;
      background: linear-gradient(to bottom, #00b4ff, transparent);
      border-radius: 2px;
    }
  }
  
  :deep(.el-input__wrapper) {
    background-color: rgba(0, 30, 60, 0.2);
    border: 1px solid rgba(0, 180, 255, 0.3);
    box-shadow: 0 0 5px rgba(0, 120, 215, 0.2);
    transition: all 0.3s;
    
    &:hover, &.is-focus {
      border-color: rgba(0, 180, 255, 0.8);
      box-shadow: 0 0 8px rgba(0, 180, 255, 0.4);
    }
    
    .el-input__inner {
      color: #e0f0ff;
    }
  }
  
  :deep(.el-select) {
    width: 100%;
  }
}

.btn-group {
  display: flex;
  gap: 15px;
  margin-bottom: 20px;
}

.tech-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px 15px;
  background: linear-gradient(to right, #004080, #0070c0);
  border-radius: 4px;
  color: #ffffff;
  cursor: pointer;
  transition: all 0.3s;
  box-shadow: 0 0 10px rgba(0, 120, 215, 0.3);
  position: relative;
  overflow: hidden;
  
  &:before {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
    transition: 0.5s;
  }
  
  &:hover:before {
    left: 100%;
  }
  
  &:hover {
    box-shadow: 0 0 15px rgba(0, 180, 255, 0.5);
    transform: translateY(-2px);
  }
  
  .el-icon {
    margin-right: 5px;
  }
}

.tech-log {
  border: 1px solid rgba(0, 150, 220, 0.3);
  border-radius: 4px;
  background-color: rgba(0, 20, 40, 0.3);
  box-shadow: inset 0 0 10px rgba(0, 30, 60, 0.5);
  
  .log-item {
    border-bottom: 1px solid rgba(0, 100, 180, 0.15);
    padding: 8px 12px;
    
    &:hover {
      background-color: rgba(0, 60, 120, 0.15);
    }
    
    .log-index {
      color: #00b4ff;
      margin-right: 10px;
      font-family: 'Consolas', monospace;
    }
    
    .log-content {
      color: #e0f0ff;
      font-family: 'Consolas', monospace;
    }
  }
}

.scroll-btn {
  display: flex;
  justify-content: center;
  margin-top: 15px;
  
  .tech-scroll-btn {
    background: linear-gradient(to right, #005080, #0090c0);
    padding: 6px 12px;
    font-size: 14px;
    
    .el-icon {
      margin-left: 5px;
      margin-right: 0;
    }
  }
}

.disabled {
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
  filter: grayscale(40%);
}

// 添加淡入淡出动画
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
 